// We need this to enable cancelling of I/O operations on
// Windows XP, Windows Server 2003 and earlier.
// Refer to "http://www.boost.org/doc/libs/1_58_0/
// doc/html/boost_asio/reference/basic_stream_socket/
// cancel/overload1.html" for details.
#ifdef WIN32
#define _WIN32_WINNT 0x0501
#endif

#include <thread>
#include <mutex>
#include <memory>
#include <iostream>
#include <system_error>
#include <string>
#include <map>

#include "asio.hpp"
#include "Trace.hpp"

namespace http_errors {
	enum http_error_codes
	{
		invalid_response = 1
	};
	
	class http_errors_category
		: public std::error_category
	{
	public:
		const char* name() const noexcept { return "http_errors"; }

		std::string message(int e) const {
			switch (e) {
			case invalid_response:
				return "Server response cannot be parsed.";
				break;
			default:
				return "Unknown error.";
				break;
			}
		}
	};
	
	const std::error_category&
		get_http_errors_category()
	{
		static http_errors_category cat;
		return cat;
	}

	std::error_code
		make_error_code(http_error_codes e)
	{
		return std::error_code(
				static_cast<int>(e), get_http_errors_category());
	}

} // namespace http_errors

namespace std {
		template<>
		struct is_error_code_enum
			<http_errors::http_error_codes> : public true_type
		{
		};
} // namespace std

class HTTPClient;
class HTTPRequest;
class HTTPResponse;

typedef void(*Callback) (const HTTPRequest& request,
	const HTTPResponse& response,
	const std::error_code& ec);


class HTTPResponse {
	friend class HTTPRequest;
	HTTPResponse() :
		m_response_stream(&m_response_buf)
	{}
public:

	unsigned int get_status_code() const {
		return m_status_code;
	}

	const std::string& get_status_message() const {
		return m_status_message;
	}

	const std::map<std::string, std::string>& get_headers() {
		return m_headers;
	}

	const std::istream& get_response() const {
		return m_response_stream;
	}

private:
	asio::streambuf& get_response_buf() {
		return m_response_buf;
	}

	void set_status_code(unsigned int status_code) {
		m_status_code = status_code;
	}

	void set_status_message(const std::string& status_message) {
		m_status_message = status_message;
	}

	void add_header(const std::string& name,
		const std::string& value)
	{
		m_headers[name] = value;
	}

private:
	unsigned int m_status_code; // HTTP status code.
	std::string m_status_message; // HTTP status message.

	// Response headers.
	std::map<std::string, std::string> m_headers;
	asio::streambuf m_response_buf;
	std::istream m_response_stream;
};

class HTTPRequest {
	friend class HTTPClient;

	static const unsigned int DEFAULT_PORT = 80;

	HTTPRequest(asio::io_context& ioc, unsigned int id) :
		m_port(DEFAULT_PORT),
		m_id(id),
		m_callback(nullptr),
		m_sock(ioc),
		m_resolver(ioc),
		m_was_cancelled(false),
		m_ioc(ioc)
	{}
	
public:
	void set_host(const std::string& host) {
		m_host = host;
	}

	void set_port(unsigned int port) {
		m_port = port;
	}

	void set_uri(const std::string& uri) {
		m_uri = uri;
	}

	void set_callback(Callback callback) {
		m_callback = callback;
	}

	std::string get_host() const {
		return m_host;
	}

	unsigned int get_port() const {
		return m_port;
	}

	const std::string& get_uri() const {
		return m_uri;
	}

	unsigned int get_id() const {
		return m_id;
	}

	void execute() {
		Trace trace(__func__);
		// Ensure that precorditions hold.
		assert(m_port > 0);
		assert(m_host.length() > 0);
		assert(m_uri.length() > 0);
		assert(m_callback != nullptr);

		// Prepare the resolving query.
		asio::ip::tcp::resolver::query resolver_query(m_host,
			std::to_string(m_port),
			asio::ip::tcp::resolver::query::numeric_service);

		std::unique_lock<std::mutex>
			cancel_lock(m_cancel_mux);

		if (m_was_cancelled) {
			cancel_lock.unlock();
			on_finish(std::error_code(
				asio::error::operation_aborted));
			return;
		}

		// Resolve the host name.
		m_resolver.async_resolve(resolver_query,
			[this](const std::error_code& ec,
			asio::ip::tcp::resolver::iterator iterator)
		{
			on_host_name_resolved(ec, iterator);
		});
	}

	void cancel() {
		Trace trace(__func__);
		std::unique_lock<std::mutex>
			cancel_lock(m_cancel_mux);

		m_was_cancelled = true;

		m_resolver.cancel();

		if (m_sock.is_open()) {
			m_sock.cancel();
		}
	}
	
private:
	void on_host_name_resolved(
		const std::error_code& ec,
		asio::ip::tcp::resolver::iterator iterator)
	{
		Trace trace(__func__);
		if (ec) {
			on_finish(ec);
			return;
		}

		std::unique_lock<std::mutex>
			cancel_lock(m_cancel_mux);

		if (m_was_cancelled) {
			cancel_lock.unlock();
			on_finish(std::error_code(
				asio::error::operation_aborted));
			return;
		}

		// Connect to the host.
		asio::async_connect(m_sock,
			iterator,
			[this](const std::error_code& ec,
			asio::ip::tcp::resolver::iterator iterator)
		{
			on_connection_established(ec, iterator);
		});

	}
	
	void on_connection_established(
		const std::error_code& ec,
		asio::ip::tcp::resolver::iterator iterator)
	{
		Trace trace(__func__);
		if (ec) {
			on_finish(ec);
			return;
		}

		// Compose the request message.
		m_request_buf += "GET " + m_uri + " HTTP/1.1\r\n";

		// Add mandatory header.
		m_request_buf += "Host: " + m_host + "\r\n";

		m_request_buf += "\r\n";

		std::unique_lock<std::mutex>
			cancel_lock(m_cancel_mux);

		if (m_was_cancelled) {
			cancel_lock.unlock();
			on_finish(std::error_code(
				asio::error::operation_aborted));
			return;
		}

		// Send the request message.
		asio::async_write(m_sock,
			asio::buffer(m_request_buf),
			[this](const std::error_code& ec,
			std::size_t bytes_transferred)
		{
			on_request_sent(ec, bytes_transferred);
		});
	}
	
	void on_request_sent(const std::error_code& ec,
		std::size_t bytes_transferred)
	{
		Trace trace(__func__);
		if (ec) {
			on_finish(ec);
			return;
		}

		m_sock.shutdown(asio::ip::tcp::socket::shutdown_send);

		std::unique_lock<std::mutex>
			cancel_lock(m_cancel_mux);

		if (m_was_cancelled) {
			cancel_lock.unlock();
			on_finish(std::error_code(
				asio::error::operation_aborted));
			return;
		}

		// Read the status line.
		asio::async_read_until(m_sock,
			m_response.get_response_buf(),
			"\r\n",
			[this](const std::error_code& ec,
			std::size_t bytes_transferred)
		{
			on_status_line_received(ec, bytes_transferred);
		});
	}
	
	void on_status_line_received(
		const std::error_code& ec,
		std::size_t bytes_transferred)
	{
		Trace trace(__func__);
		if (ec) {
			on_finish(ec);
			return;
		}

		// Parse the status line.
		std::string http_version;
		std::string str_status_code;
		std::string status_message;

		std::istream response_stream(
			&m_response.get_response_buf());
		response_stream >> http_version;

		if (http_version != "HTTP/1.1"){
			// Response is incorrect.
			on_finish(http_errors::invalid_response);
			return;
		}

		response_stream >> str_status_code;

		// Convert status code to integer.
		unsigned int status_code = 200;

		try {
			status_code = std::stoul(str_status_code);
		}
		catch (std::logic_error&) {
			// Response is incorrect.
			on_finish(http_errors::invalid_response);
			return;
		}

		std::getline(response_stream, status_message, '\r');
		// Remove symbol '\n' from the buffer.
		response_stream.get();

		m_response.set_status_code(status_code);
		m_response.set_status_message(status_message);

		std::unique_lock<std::mutex>
			cancel_lock(m_cancel_mux);

		if (m_was_cancelled) {
			cancel_lock.unlock();
			on_finish(std::error_code(
				asio::error::operation_aborted));
			return;
		}

		// At this point the status line is successfully
		// received and parsed.
		// Now read the response headers.
		asio::async_read_until(m_sock,
			m_response.get_response_buf(),
			"\r\n\r\n",
			[this](
			const std::error_code& ec,
			std::size_t bytes_transferred)
		{
			on_headers_received(ec,
				bytes_transferred);
		});
	}
	
	void on_headers_received(const std::error_code& ec,
		std::size_t bytes_transferred)
	{
		Trace trace(__func__);
		if (ec) {
			on_finish(ec);
			return;
		}

		// Parse and store headers.
		std::string header, header_name, header_value;
		std::istream response_stream(
			&m_response.get_response_buf());

		while (true) {
			std::getline(response_stream, header, '\r');

			// Remove \n symbol from the stream.
			response_stream.get();

			if (header == "")
				break;

			size_t separator_pos = header.find(':');
			if (separator_pos != std::string::npos) {
				header_name = header.substr(0,
					separator_pos);

				if (separator_pos < header.length() - 1)
					header_value =
					header.substr(separator_pos + 1);
				else
					header_value = "";

				m_response.add_header(header_name,
					header_value);
			}
		}

		std::unique_lock<std::mutex>
			cancel_lock(m_cancel_mux);

		if (m_was_cancelled) {
			cancel_lock.unlock();
			on_finish(std::error_code(
				asio::error::operation_aborted));
			return;
		}

		// Now we want to read the response body.
		asio::async_read(m_sock,
			m_response.get_response_buf(),
			[this](
			const std::error_code& ec,
			std::size_t bytes_transferred)
		{
			on_response_body_received(ec,
				bytes_transferred);
		});

		return;
	}
	
	void on_response_body_received(
		const std::error_code& ec,
		std::size_t bytes_transferred)
	{
		Trace trace(__func__);
		if (ec == asio::error::eof)
			on_finish(std::error_code());
		else
			on_finish(ec);
	}
	
	void on_finish(const std::error_code& ec)
	{
		Trace trace(__func__);
		if (ec) {
			std::cout << "Error occured! Error code = "
				<< ec.value()
				<< ". Message: " << ec.message();
		}

		m_callback(*this, m_response, ec);

		return;
	}

private:
	// Request paramters. 
	std::string m_host;
	unsigned int m_port;
	std::string m_uri;

	// Object unique identifier. 
	unsigned int m_id;

	// Callback to be called when request completes. 
	Callback m_callback;

	// Buffer containing the request line.
	std::string m_request_buf;

	asio::ip::tcp::socket m_sock;
	asio::ip::tcp::resolver m_resolver;

	HTTPResponse m_response;

	bool m_was_cancelled;
	std::mutex m_cancel_mux;

	asio::io_context& m_ioc;
};

class HTTPClient {
public:
	HTTPClient(){
		m_work.reset(new asio::io_context::work(m_ioc));

		m_thread.reset(new std::thread([this](){
			m_ioc.run();
		}));
	}

	std::shared_ptr<HTTPRequest>
		create_request(unsigned int id)
	{
		Trace trace(__func__);
		return std::shared_ptr<HTTPRequest>(
			new HTTPRequest(m_ioc, id));
	}

	void close() {
		Trace trace(__func__);
		// Destroy the work object. 
		m_work.reset(NULL);

		// Waiting for the I/O thread to exit.
		m_thread->join();
	}

private:
	asio::io_context m_ioc;
	std::unique_ptr<asio::io_context::work> m_work;
	std::unique_ptr<std::thread> m_thread;
};

void handler(const HTTPRequest& request,
const HTTPResponse& response,
const std::error_code& ec)
{
	Trace trace(__func__);
	if (!ec) {
		std::cout << "Request #" << request.get_id()
			<< " has completed. Response: "
			<< response.get_response().rdbuf();
	}
	else if (ec == asio::error::operation_aborted) {
		std::cout << "Request #" << request.get_id()
			<< " has been cancelled by the user."
			<< std::endl;
	}
	else {
		std::cout << "Request #" << request.get_id()
			<< " failed! Error code = " << ec.value()
			<< ". Error message = " << ec.message()
			<< std::endl;
	}

	return;
}

int main()
{
	Trace trace(__func__);
	try {
		HTTPClient client;

		trace.debug("request_one");

		std::shared_ptr<HTTPRequest> request_one =
			client.create_request(1);

		request_one->set_host("localhost");
		request_one->set_uri("/index.html");
		request_one->set_port(3333);
		request_one->set_callback(handler);

		request_one->execute();

		trace.debug("request_two");

		std::shared_ptr<HTTPRequest> request_two =
			client.create_request(1);

		request_two->set_host("localhost");
		request_two->set_uri("/example.html");
		request_two->set_port(3333);
		request_two->set_callback(handler);

		request_two->execute();

		request_two->cancel();

		// Do nothing for 15 seconds, letting the
		// request complete.
		std::this_thread::sleep_for(std::chrono::seconds(15));

		// Closing the client and exiting the application.
		client.close();
	}
	catch (std::system_error &e) {
		std::cout << "Error occured! Error code = " << e.code()
			<< ". Message: " << e.what();

		return e.code().value();
	}

	return 0;
}
